<?php
/**
 * @link https://www.kancloud.cn/cleverstone/ymb2
 * @copyright Copyright (c) 2020 Yii Manager Software LLC
 */

namespace common\components;

use Yii;
use yii\base\Component;
use yii\base\Event;
use yii\helpers\Url;
use yii\web\Controller;
use yii\base\InvalidConfigException;
use yii\caching\FileCache;
use yii\di\Instance;
use yii\helpers\ArrayHelper;
use yii\helpers\FileHelper;
use yii\rbac\CheckAccessInterface;
use yii\caching\CacheInterface;
use common\models\SystemMenu;
use common\models\Admin;
use common\models\SystemAuthRelation;
use common\handlers\backend\AfterActionRecordOperationLog;
use builder\base\RbacValidateInterface;
use builder\base\BaseController;

/**
 * RBAC管理器
 * @author cleverstone
 * @since ym2.0
 */
class RbacManager extends Component implements CheckAccessInterface, RbacValidateInterface
{
    /**
     * @var CacheInterface|array|string the cache used to improve RBAC performance. This can be one of the following:
     *
     * - an application component ID (e.g. `cache`)
     * - a configuration array
     * - a [[\yii\caching\Cache]] object
     *
     * When this is not set, it means caching is not enabled.
     *
     * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
     * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
     * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
     * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
     *
     * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
     * you have to manually call [[invalidateCache()]] to ensure data consistency.
     *
     * @since 2.0.3
     */
    public $cache = 'cache';
    /**
     * @var string
     */
    public $cachePath = '@backend/runtime/cache';
    /**
     * @var string the key used to store RBAC data in cache
     * @see cache
     * @since 2.0.3
     */
    public $cacheKey = 'RBAC';
    /**
     * @var array 角色权限缓存
     */
    private static $_permissions = [];
    /**
     * @var null|array 本地定义
     */
    private static $_localDefinition = [];
    /**
     * @var int 排序
     */
    private static $_sorter = 0;

    /**
     * Initialize
     * @throws \yii\base\Exception
     * @throws \yii\base\InvalidConfigException
     */
    public function init()
    {
        $cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
        /* @var FileCache $cache */
        if ($cache instanceof FileCache) {
            $this->cache = clone $cache;
            $cachePath = Yii::getAlias($this->cachePath);
            if (!is_dir($cachePath)) {
                FileHelper::createDirectory($cachePath, 0775, true);
            }

            $this->cache->cachePath = $cachePath;
        }
    }

    /**
     * 清除管理组缓存权限
     * @param null|array|int $group 管理组ID
     */
    public function invalidateCachePermissions($group = null)
    {
        if ($group === null) {//清除所有管理组权限缓存
            $this->cache->delete($this->cacheKey);
        } else {//清除指定管理组权限缓存
            $group = (array)$group;

            $cachePermissions = $this->cache->get($this->cacheKey);
            foreach ($group as $itemID) {
                if (!empty($cachePermissions) && !empty($cachePermissions[$itemID])) {
                    unset($cachePermissions[$itemID]);
                }
            }

            $this->cache->set($this->cacheKey, $cachePermissions);
        }

        self::$_permissions = [];
    }

    /**
     * 从缓存中加载当前管理组权限
     * @param int $group 管理组ID
     * @return array
     */
    public function loadPermissionsFromCache($group)
    {
        if (!empty(self::$_permissions)) {
            return self::$_permissions;
        }

        $cachePermission = $this->cache->get($this->cacheKey);
        if (!empty($cachePermission) && !empty($cachePermission[$group])) {
            self::$_permissions = $cachePermission[$group];
            return self::$_permissions;
        }

        self::$_permissions = $this->getPermissionsFromDb($group);
        if (empty($cachePermission)) {
            $cachePermission = [];
        }

        $cachePermission[$group] = self::$_permissions;
        $this->cache->set($this->cacheKey, $cachePermission);

        return self::$_permissions;
    }

    /**
     * 从数据库中获取角色权限
     * @param int $group 管理组ID
     * @return array
     */
    public function getPermissionsFromDb($group)
    {
        if ($group == Admin::ADMIN_GROUP) {
            return SystemMenu::query()->orderBy(['sort' => SORT_ASC, 'id' => SORT_DESC])->all();
        } else {
            return SystemAuthRelation::query('m.*', 'r')
                ->leftJoin(['m' => SystemMenu::tableName()], 'r.menu_id=m.id')
                ->where(['r.group_id' => $group])
                ->orderBy(['m.sort' => SORT_ASC, 'm.id' => SORT_DESC])
                ->all();
        }
    }

    /**
     * 加载本地菜单定义
     * @param bool $fromCache
     * @return array|null
     * @throws InvalidConfigException
     */
    public function loadLocalDefinition($fromCache = true)
    {
        $localDefinition = self::$_localDefinition;
        if (!empty($localDefinition) && $fromCache) {
            return $localDefinition;
        }

        $localDefinition = require Yii::getAlias('@backend/config/menu.php');
        $this->checkLocalMenuUnique($localDefinition);

        self::$_localDefinition = $localDefinition;
        return $localDefinition;
    }

    /**
     * 检查是否存在重复定义的菜单
     * @param array $localMenu 菜单定义
     * @param array $tmpLinks 临时存储
     * @throws InvalidConfigException
     */
    protected function checkLocalMenuUnique($localMenu, &$tmpLinks = [])
    {
        foreach ($localMenu as $item) {
            if (in_array($item['link'], $tmpLinks)) {
                throw new InvalidConfigException('The defined menu cannot be repeated.');
            }

            array_push($tmpLinks, $item['link']);

            if (!empty($item['children'])) {
                $this->checkLocalMenuUnique($item['children'], $tmpLinks);
            }
        }
    }

    /**
     * 更新系统菜单
     * @return true|string
     * @throws InvalidConfigException
     */
    public function updateSystemMenu()
    {
        $localMenu = $this->loadLocalDefinition(false);

        $trans = Yii::$app->db->beginTransaction();
        try {
            //create/update
            $haystack = [];
            $this->updateSystemMenuRecursive($localMenu, $haystack);

            //delete
            $all = SystemMenu::query(['link'])->indexBy('id')->column();
            $deleteMenu = [];
            foreach ($all as $id => $link) {
                if (!in_array($link, $haystack)) {
                    $deleteMenu[] = $id;
                }
            }

            if (!empty($deleteMenu)) {
                //从菜单中删除
                SystemMenu::deleteAll(['id' => $deleteMenu]);
                //从权限关联表中删除
                SystemAuthRelation::deleteAll(['menu_id' => $deleteMenu]);
            }

            $trans->commit();

            // 清除缓存权限
            $this->invalidateCachePermissions();

            return true;
        } catch (\Throwable $e) {
            $trans->rollBack();
            return $e->getMessage();
        }
    }

    /**
     * 本地菜单递归式CU
     * @param array $localMenu 本地菜单
     * @param array $haystack 本地定义的权限
     * @param int $pid 父ID
     */
    protected function updateSystemMenuRecursive(array $localMenu, &$haystack, $pid = 0)
    {
        foreach ($localMenu as $item) {
            self::$_sorter++;

            $haystack[] = $link = $item['link'];
            $model = SystemMenu::findOne(['link' => $link]);

            if (empty($model)) {
                //新增
                $model = new SystemMenu();
                $model->setScenario('SCE_Menu_Create');
            } else {
                //更新
                $model->setScenario('SCE_Menu_Update');
            }

            $model->setAttributes($item);
            $model->sort = self::$_sorter;
            $model->pid = $pid;
            if (!$model->save()) {
                throw new \yii\db\Exception($model->error);
            }

            if (!empty($item['children'])) {
                $this->updateSystemMenuRecursive($item['children'], $haystack, $model->id);
            }
        }
    }

    /**
     * {{@inheritdoc}}
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        /* @var Admin $user */
        if ($user = Yii::$app->user->identity) {
            $this->loadPermissionsFromCache($user->group);
            $canPermissions = self::$_permissions;
            if (in_array($permissionName, ArrayHelper::getColumn($canPermissions, 'link'), true)) {
                /* 记录操作日志 */
                Event::on(
                    Controller::class,
                    Controller::EVENT_AFTER_ACTION,
                    [new AfterActionRecordOperationLog(), 'run'],
                    (object)compact('user', 'permissionName', 'canPermissions'),
                    false
                );

                return true;
            }
        }

        return false;
    }

    /**
     * {{@inheritdoc}}
     */
    public function permissionValidate($route)
    {
        //外部链接，直接通过权限验证。
        if (!Url::isRelative($route)) {
            return true;
        }

        //检查路由是否符合权限验证条件。不符合条件的，返回true！
        $parts = Yii::$app->createController($route);
        if (is_array($parts)) {
            list($controller, $actionID) = $parts;
            if ($controller instanceof BaseController) {
                if (
                    in_array($actionID, $controller->publicActions, true)
                    || in_array($actionID, $controller->guestActions, true)
                    || in_array($actionID, $controller->undetectedActions, true)
                ) {
                    return true;
                }
            }
        }

        //检查用户权限。
        /* @var Admin $user */
        if ($user = Yii::$app->user->identity) {
            $this->loadPermissionsFromCache($user->group);
            $canPermissions = self::$_permissions;
            if (in_array($route, ArrayHelper::getColumn($canPermissions, 'link'), true)) {
                return true;
            }
        }

        return false;
    }
}